底层同一套(epoll),上层不同皮——全自动 vs 兼容老 API vs 显式染色
| Go goroutine | Java 虚拟线程 | Kotlin 协程 | |
|---|---|---|---|
| 底层引擎 | epoll / kqueue | epoll / kqueue | epoll / kqueue |
| 调度方式 | 抢占式 + 协作式 | 抢占式 + 协作式 | 纯协作式 |
| 挂起点 | 你不知道在哪 | 你不知道在哪 | 你写了 suspend 才挂 |
| 有无染色函数 | 无 | 无 | 有(suspend 关键字) |
| 栈 | 堆上,按需增长 | 堆上,按需增长 | 堆上续体对象 |
| 代码风格 | 同步阻塞风格 | 同步阻塞风格 | suspend + 显式切换 |
func handle(conn net.Conn) {
buf := make([]byte, 1024)
n, _ := conn.Read(buf) // 看起来阻塞,编译器自动插入挂起点
conn.Write(buf[:n]) // 同上
}
// 起 10 万个,毫无压力
for i := 0; i < 100000; i++ {
go handle(conn) // go 关键字,够简单
}
Go 的每一个函数调用入口都是一个潜在的挂起点(栈检查 safepoint)。你写的 conn.Read()看起来阻塞,编译器帮你切成状态机,你完全不知道——也不需要知道。
Go 的哲学:阻塞式代码最好写,那就把一切搞成看起来阻塞、实际非阻塞。编译器全包。
// 和普通 Thread 写法一模一样
Thread.startVirtualThread(() -> {
socketChannel.read(buffer); // 看起来阻塞
process(buffer);
});
// 老代码不用改,只需换一个工厂方法
// 老:
ExecutorService pool = Executors.newCachedThreadPool(); // 平台线程池
// 新:
ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); // 虚拟线程池
// 同一套 API,底层从 OS 线程切换到了用户态调度
挂起怎么发生?socketChannel.read(buffer)→ 底层调 read(fd, buf)→ 发现数据还没到 → 不阻塞 OS 线程,把 VirtualThread 的 Continuation yield 出去 → OS 线程被释放跑另一个 VirtualThread → epoll 通知数据到了 → Continuation 恢复 → read 返回。
Java 的哲学:二十年老祖宗代码都是 Thread,那就让 Thread 变轻。不改 API,只改底层。
suspend fun fetchData(): String { // ← suspend 关键字!
delay(1000)
return "data"
}
// 不标记 suspend 就不能调 delay
fun normalFunc() {
delay(1000) // 编译错误!不在 suspend 函数里不能调挂起函数
}
Kotlin 把挂起点染了色(colored function):有 suspend 的函数才可能挂起;没有的一定不挂起,放心。
suspend fun a() { b() } // suspend 调 suspend
suspend fun b() { delay(100) } //
fun c() {
a() // 编译错误!普通函数不能调 suspend
delay(100) // 编译错误!
}
// 必须这样:
fun c() = runBlocking { // 搭个桥
a() //
}
// Go:所有函数都能挂起(里面有没有 I/O 你根本看不出来)
func a() { b() } // b 会挂起,a 也自动会挂起
func b() { conn.Read(buf) } // 看起来是普通函数,实际挂起
func c() {
a() // 普通函数,可以调
}
// Java:也没有染色,完全兼容老 API
void a() { b(); } //
void b() { socket.read(); } // 看起来阻塞,在虚拟线程里挂起
void c() {
Thread.startVirtualThread(() -> a()); // 普通方法,不改任何签名
}
| Go goroutine | Java 虚拟线程 | Kotlin 协程 | |
|---|---|---|---|
| 挂起点 | 每函数入口自动插入 | 每个可能阻塞的调用自动 | 只有 suspend 函数 |
| 是否染色 | 无 | 无 | 有(suspend 关键字) |
| 感知性 | 完全无感知 | 完全无感知 | 你明确知道哪里会挂 |
| API 兼容 | 新语言,全新 API | 和 Thread API 100% 兼容 | Kotlin 生态全新 API |
| 抢占式 | 每 10ms 强制切换 | safepoint 强制切换 | 纯协作(你让才让) |
| 创建方式 | go func() | Thread.startVirtualThread() | launch { } |
| 栈实现 | 分段栈 → 连续栈 | Continuation 对象链 | Continuation 对象链 |
| 取消 | context.WithCancel | Thread.interrupt() | Job.cancel() + 结构化并发 |
Kotlin 是真正信「协作」的——你不在代码里主动让(写 suspend),它就真不让。Go 和 Java 是表面协作、实际给你兜底。
| 语言 | 最适合 | 核心优势 |
|---|---|---|
| Go | 服务端、中间件 | 并发是语言一等公民,go 就完了 |
| Java | 老项目上百万并发 | 换线程池即可,不改任何业务代码 |
| Kotlin | Android / 高控制力场景 | 协程 + 结构化并发,精确把控生命周期 |
Go:并发是语言的一等公民。Java:让 Thread 变轻,不改 API。Kotlin:显式染色 + 结构化并发,精确把控。